from math import sqrt
from utils.misc import INF

from stacking.instance.generation.simgen import SimGen
from stacking.instance.generation.simgen_nr import normalized_relocations
from stacking.instance.generation.simgen_ips import items_per_stack_ratio


class SimGenNRIPS(SimGen):
    """Simulation-based instance generator that uses both Normalized Relocations AND Items-Per- 
    -Stack ratio to evaluate candidate sets of items. The combination of the two indicators NR and 
    IPS can be controlled using the accept_fnc() parameter. The prototype for accept_fnc() is
        accept_fnc(simgen, items, nstacks, NR, IPS) -> (accepted, delta)
    For instance, inside the accept_fnc, one can use NR as first sorting criterion, and use IPS 
    for tie breaking, or use a weighted sum of both scores."""
    default_target = 2.0
    default_epsilon = 0.5
    initial_delta = INF
    
    def configure(self, solve_fnc=None, accept_fnc=None, target=None, epsilon=None):
        if solve_fnc is None:
            raise Exception("missing keyword argument solve_fnc")
        self.solve_fnc  = solve_fnc
        self.accept_fnc = accept_fnc or type(self).accept
        self.target     = target  if target  is not None else self.default_target
        self.epsilon    = epsilon if epsilon is not None else self.default_epsilon
        self.best.delta = self.initial_delta  # initial value of delta must be comparable with 
                                              # the second value returned by accept_fnc()
        
    def analyze(self, items, nstacks):
        """Returns True if the given item list matches the specified IPS ratio interval. If the 
        observed ratio is too different from the target (more than the allowed epsilon), the 
        production interval is adjusted and the method returns False.""" 
        NR  = normalized_relocations(self.solve_fnc, items, nstacks)
        IPS = items_per_stack_ratio(items, nstacks)
        return self.accept_fnc(self, items, nstacks, NR, IPS)
        
    def accept(self, items, nstacks, NR, IPS):
        raise NotImplementedError()
        
        
class SimGenNRIPS1(SimGenNRIPS):
    """This version of the NR-IPS generator uses NR as first criteria, and IPS for tie breaking.
    The rule for adjusting production interval is to use the geometric mean of the ratio 'obs/tgt'
    for both NR and IPS."""
    default_target  = (1.0, 10.0)
    default_epsilon = (0.1, 0.5)
    initial_delta   = (INF, INF)
    
    def accept(self, items, nstacks, NR, IPS):
        delta = (abs(NR - self.target[0]), abs(IPS - self.target[1]))
        if delta[0] <= self.epsilon[0] and delta[1] <= self.epsilon[1]:
            return True, delta
        PI_current = self.production_interval
        PI_target  = PI_current * sqrt(NR / self.target[0] * IPS / self.target[1])
        self.adjust_production_interval(PI_target)
        return False, delta
        
        
class SimGenNRIPS2(SimGenNRIPS):
    """This version of the NR-IPS generator uses the geometric mean of NR and IPS deltas to 
    compute its own aggregate delta. Therefore both indicators affect how the generator considers 
    a set of items to be better than another."""
    default_target  = 5.0
    default_epsilon = 0.25
    NR_power  = 1
    IPS_power = 1
    
    def accept(self, items, nstacks, NR, IPS):
        NR_term  = (NR  ** self.NR_power)
        IPS_term = (IPS ** self.IPS_power)
        combined = (NR_term * IPS_term) ** (1.0 / (self.NR_power + self.IPS_power))
        delta    = abs(combined - self.target)
        self.obs_tseries.collect(combined)
        self.tgt_tseries.collect(self.target)
        if delta <= self.epsilon:
            return True, delta
        self.adjust_production_interval(self.production_interval * combined / self.target)
        return False, delta
        
        
SimGen.NRIPS = SimGenNRIPS
SimGen.NRIPS1 = SimGenNRIPS1
SimGen.NRIPS2 = SimGenNRIPS2
